iT邦幫忙

2026 iThome 鐵人賽

DAY 27
0
Software Development

從登入到授權-現代軟體的身分架構指南系列 第 27 篇

Day 27|被劫截的授權碼:中間人如何攻擊 OAuth/OIDC

  • 分享至 

  • xImage
  •  

前言

前一篇整理了 JIT Provisioning 與 SCIM,從首次登入時的帳號開通,一路談到後續的帳號同步、異動與停用。接下來要換個角度,從安全攻防來看 OAuth 2.0 與 OpenID Connect(OIDC)流程中可能出現的風險。

本篇會先從中間人攻擊(MITM)的概念開始,再逐步拉回 OAuth 2.0/OIDC 的 Authorization Code Flow,說明 state、nonce 與 PKCE 各自防範的風險,以及 RFC 9700 對這類攻擊提出的安全建議。

今天內容涵蓋:

  1. 什麼是中間人攻擊(MITM):概念與常見手法
  2. OAuth 2.0/OIDC Flow 中的攻擊點
  3. State 參數:防範登入 CSRF/Code Injection
  4. PKCE:防範授權碼攔截
  5. Nonce 參數:防範 ID Token Replay
  6. RFC 9700:OAuth 2.0 安全最佳實踐

一、什麼是中間人攻擊(MITM):概念與常見手法

中間人攻擊(Man-in-the-Middle Attack,MITM)指的是攻擊者站在使用者與網站/服務中間,讓雙方都以為連線正常。實際上,資料會先經過攻擊者,再被轉送到另一端。

攻擊者不一定只是偷看資料,也可能攔截、修改或重送資料。常見情境包括:偽造 Wi-Fi 熱點、ARP Spoofing、DNS Spoofing、HTTPS Stripping、Session Hijacking,以及透過偽造憑證的 SSL/TLS 攔截代理介入連線。

https://ithelp.ithome.com.tw/upload/images/20260916/20181928hTInd4rebJ.png

這些手法發生的位置不同,但共同點都是:原本應該直接往來的通訊,被攻擊者插入中間。也因為雙方表面上都還能正常收發資料,使用者不一定會立刻察覺異常。

所以後面談 OAuth 2.0/OIDC 時,不能只看「有沒有 HTTPS」。HTTPS 很重要,但授權請求、回調、授權碼交換與 Token 驗證這些流程,也都需要防止資料被攔截、竄改或重放。


二、OAuth 2.0/OIDC Flow 中的攻擊點

回到 Authorization Code Flow 來看,OAuth 2.0/OIDC 的登入流程不是只有「使用者登入」這一步,而是由授權請求、授權回應、Code 交換、Token 發行,以及後續使用 Access Token 存取資源組成。

這張圖先把流程拆開來看:左邊是正常流程,右邊標出每個階段可能出現的風險。

https://ithelp.ithome.com.tw/upload/images/20260916/201819281A12BvCSzk.png

可以先抓住四個重點:

圖中的階段 主要風險 後面對應的防護
① 授權請求 登入 CSRF/Code Injection:攻擊者把自己的登入結果或授權碼塞進受害者的流程 state
② 授權回應(重導) 授權碼攔截:Authorization Code 在回到 Client 的途中被拿走 PKCE
③ Code 交換 攔截後直接換 Token:如果只有授權碼就能換 Token,攻擊者就可能搶先兌換 PKCE
④ Token 發行(OIDC) ID Token Replay:舊的 ID Token 被重新拿來冒用身分 nonce

state、PKCE、nonce 之所以容易看起來很像,是因為它們都會在流程前段產生一個不容易被猜到的值,並在後面拿來檢查。不過它們出現的位置和驗證時機不同:

機制 在哪裡產生 放在哪個流程 什麼時候驗證 驗證目的
state Client 發起授權請求前 授權請求,之後由 Authorization Server 原樣帶回授權回應 Client 收到 redirect_uri 回調時 確認授權回應是否屬於同一次流程
PKCE Client 發起授權請求前產生 code_verifier,並算出 code_challenge 授權請求只放 code_challenge;Code 交換時才送出 code_verifier Authorization Server 的 Token 端點收到 Code 交換請求時 確認拿 authorization code 換 Token 的是原本的 Client
nonce Client 發起 OIDC 授權請求前 授權請求,之後由 Authorization Server 寫入 ID Token Client 收到並驗證 ID Token 時 確認 ID Token 是否為本次登入流程簽發

因此,這一篇後面會照著圖上的攻擊點往下看:先看 state 如何把授權請求與授權回應配對,再看 PKCE 如何防止被攔截的 authorization code 被拿去換 Token,最後看 nonce 如何防止舊的 ID Token 被重新使用。至於圖中後續使用 Access Token 存取 Resource Server 的部分,會留到下一篇討論被竊取的 Access Token 要怎麼限制使用。

這些情境也呼應 RFC 9700 的安全建議:防護不能只放在單一環節,而要涵蓋授權請求、授權回應、Code 交換與 Token 驗證等不同階段。


三、State 參數:防範登入 CSRF/Code Injection

state 對應的是圖中的第 ①、② 階段:Client 發出授權請求,以及 Authorization Server 透過 redirect_uri 把使用者導回 Client 的授權回應。

它要解決的問題是:Client 收到回應時,如何確認「這個回應真的接得上剛剛那次授權請求」,而不是攻擊者另外塞進來的回調。

正常流程如下:

  1. Client 在發出授權請求前,產生一組不可猜的 state,並暫存在使用者的 session 中。
  2. Client 把 state 放進授權請求,一起送到 Authorization Server。
  3. 使用者完成登入與授權後,Authorization Server 透過 redirect_uri 導回 Client,並把同一組 state 原樣帶回。
  4. Client 收到授權回應後,先比對回來的 state 是否存在,且是否與 session 中保存的 state 一致。
  5. 只有 state 驗證通過,Client 才繼續處理 authorization code;如果 state 遺失或不一致,就直接拒絕。

攻擊情境:攻擊者先自己走一遍授權流程,拿到一組屬於攻擊者帳號的有效 authorization code,再把這組 code 包進回調連結中,例如 https://app.com/callback?code=ATTACKER_CODE,誘騙受害者點擊。若 Client 沒有檢查 state,就可能把這個外部塞進來的回應當成合法流程處理,導致受害者的登入狀態被綁定到攻擊者帳號,形成登入 CSRF(Cross-Site Request Forgery,跨站請求偽造)或 Code Injection(授權碼注入)。

所以,state 保護的重點是「回應是否屬於同一次授權流程」。它不負責保護 authorization code 本身不被偷走;如果 code 已經在回到 Client 的途中被攔截,那是下一節 PKCE 要處理的問題。

📘RFC 9700 明確要求:客戶端必須防範 CSRF。若客戶端確認授權伺服器支援 PKCE,可以直接依賴 PKCE 提供的 CSRF 保護;在 OIDC 流程中 nonce 也能提供 CSRF 保護;否則就必須使用綁定到 user agent 的一次性 state。


四、PKCE:防範授權碼攔截

PKCE 對應的是圖中的第 ②、③ 階段:Authorization Code 回到 Client 的路上,以及 Client 拿 Authorization Code 去交換 Token 的過程。

它和 state 看起來相似,都是在把前後流程接起來;但兩者檢查的重點不同:

  • state 檢查的是:這個授權回應是不是屬於同一次登入流程。
  • PKCE 檢查的是:拿 authorization code 來換 Token 的人,是不是當初發起流程的同一個 Client。

攻擊情境:如果攻擊者在授權回應階段攔截到 Authorization Code,例如行動裝置上有惡意 App 註冊了相同的 URL scheme(例如 my-app://callback),就可能搶先拿這組 code 去 Token 端點交換 Token。若 Authorization Server 只檢查 code 本身,攻擊者就有機會成功。

PKCE 的做法是讓 Client 在一開始就準備一組「只有自己知道的驗證值」:

  1. Client 在送出授權請求前,產生一組夠隨機、很難被猜到的 code_verifier。
  2. Client 用 code_verifier 算出 code_challenge。
  3. 授權請求只送出 code_challenge,不送出 code_verifier。
  4. Authorization Server 先記住這次授權請求對應的 code_challenge。
  5. 等到 Code 交換階段,Client 才把原本的 code_verifier 送到 Token 端點。
  6. Authorization Server 重新計算 code_verifier,確認它是否能對上先前保存的 code_challenge。

code_verifier 之所以能作為防護,是因為它不會出現在前面的瀏覽器重導流程中。攻擊者即使在 redirect_uri 回來時攔截到 authorization code,通常也只能拿到 code,看不到一開始保存在 Client 端的 code_verifier。沒有 code_verifier,就無法完成 Token 交換。

因此,PKCE 的重點不是讓 authorization code 不會被偷,而是讓「偷到 code 的人仍然不能用它換 Token」。

📘RFC 9700 要求公開客戶端必須使用 PKCE,機密客戶端也建議使用。challenge 方法必須使用 S256,避免使用 plain;授權伺服器也必須避免 PKCE 降級攻擊,不能讓攻擊者把原本需要 code_verifier 的流程降級成不檢查。


五、Nonce 參數:防範 ID Token Replay

nonce 對應的是圖中的第 ④ 階段:Authorization Server 發出 ID Token,Client 接收並驗證登入結果的時候。

nonce 的重點在於 ID Token 本身。即使 ID Token 的簽章有效,也只能代表它曾經由 Authorization Server 簽發;Client 仍需要確認它是不是本次登入流程產生的身分結果。也就是說,這裡要防的是「舊的 ID Token 被重新拿來使用」,而不是前面 state 處理的回調配對問題。

攻擊流程

ID Token 是 OIDC 用來表示使用者身分的 Token。因為它有 Authorization Server 的簽章,所以 Client 會依據它判斷使用者是誰。問題是:簽章有效只代表「這個 ID Token 曾經由 Authorization Server 簽發」,不代表「它一定是這一次登入流程產生的」。

攻擊流程可能會長這樣:

  1. 攻擊者先取得一組過去簽發過、而且簽章有效的 ID Token。
  2. 受害者之後啟動新的 OIDC 登入流程。
  3. 攻擊者在流程中重放先前取得的 ID Token,讓 Client 收到一個看起來有效的登入結果。
  4. 如果 Client 只驗證 ID Token 的簽章、issuer、audience 與有效時間,卻沒有確認它是否屬於本次流程,就可能接受這個舊 Token。
  5. 結果是,Client 把被重放的 ID Token 當成本次登入結果,造成身分被冒用。

防禦方式

nonce 的做法,是讓 Client 在授權請求階段先產生一組不可猜的 nonce,並把它與本次登入流程一起保存。接著,Client 把 nonce 放進授權請求送給 Authorization Server。

Authorization Server 簽發 ID Token 時,會把同一組 nonce 寫進 ID Token。Client 收到 ID Token 後,除了驗證簽章、issuer、audience、有效時間等資訊,也要檢查 ID Token 裡的 nonce 是否與自己本次流程保存的 nonce 一致。

如果 nonce 不一致,表示這個 ID Token 不是針對本次登入流程簽發;即使它的簽章有效,也應該拒絕。

因此,nonce 的重點不是確認「回調是不是同一次流程」,而是確認「ID Token 是不是本次流程產生的身分結果」。這也是它和 state 最大的差異。

📘RFC 9700 提到,在符合特定條件的機密客戶端中,nonce 可以提供與 PKCE 類似的保護效果;但對公開客戶端而言,PKCE 仍然是必要防護。實作時可以把 nonce 視為 OIDC 用來防止 ID Token Replay 的重要檢查。


六、RFC 9700:OAuth 2.0 安全最佳實踐

RFC 9700(BCP 240,Best Current Practice for OAuth 2.0 Security)是 OAuth 2.0 的安全最佳實踐文件,於 2025 年 1 月發布。它整理了 RFC 6749、RFC 6750、RFC 6819 之後累積的安全經驗,並針對實務上常見的攻擊手法,明確建議應採取的防護措施。

對本文討論的 MITM 與授權流程攻擊而言,RFC 9700 的重點不只是要求使用單一防護機制,而是要求 Client、Authorization Server 與 Resource Server 在不同階段各自落實必要檢查。以下整理幾項與本文主題相關的重點:

  • redirect_uri 必須完整比對:Authorization Server 驗證 redirect_uri 時,應採用完整且精確的比對方式,避免只比對前綴或部分字串,降低授權碼被導向惡意端點的風險。
  • 避免開放式重定向(Open Redirector):Client 不應提供可任意轉向外部網址的 redirect 端點,否則可能被攻擊者利用來轉送 authorization code 或 Token。
  • 防範 Mix-up Attack:當 Client 同時支援多個 Authorization Server 時,應確認授權回應來自預期的伺服器;實務上可透過 iss 參數(RFC 9207)降低回應來源被混淆的風險。
  • 避免使用 Implicit Grant:Access Token 不應直接出現在授權回應中,因為這會增加 Token 暴露在瀏覽器、URL 或中間人攻擊中的機率。
  • 公開客戶端應使用 PKCE:對於行動 App、SPA 等無法安全保存 client secret 的公開客戶端,應使用 PKCE,並採用 S256 作為 code_challenge_method。
  • 停用不安全的授權方式:ROPC 等要求使用者直接把密碼交給 Client 的流程,已不符合現代 OAuth 2.0 安全實務,應避免繼續使用。

也就是說,RFC 9700 的重點不是新增單一機制,而是把 OAuth 2.0 流程中各階段該做的安全檢查收斂成一組實務基準。


小結

對 OAuth 2.0/OIDC 而言,MITM 風險不只存在於網路傳輸層,也可能出現在授權請求、授權回應、授權碼交換與 Token 驗證等不同階段。

本文提到的 state、PKCE 與 nonce,分別對應登入 CSRF/Code Injection、授權碼攔截,以及 ID Token Replay。它們不是彼此替代的選項,而是在不同位置補上不同的安全檢查。

RFC 9700 則把這些防護整理成 OAuth 2.0 的安全最佳實踐,提醒實作者除了正確使用 state、PKCE 與 nonce,也要落實 redirect_uri 精確比對、避免不安全的授權方式,並持續檢視 Token 在後續使用階段可能面臨的風險。

下一篇會接著看 Access Token 發出之後的風險:如果 Token 真的被偷走,如何透過權限範圍、短效期限,以及 mTLS/DPoP 這類 Sender-Constrained Token 機制,限制它被攻擊者直接冒用。


參考資源


上一篇
Day 26|JIT 與 SCIM:從首次登入佈建到帳號生命週期管理
下一篇
Day 28|被偷走的 Access Token:如何限制 Token 被冒用?
系列文
從登入到授權-現代軟體的身分架構指南 共 30 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言